/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.contacts.common.list;

import android.app.Fragment;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Parcelable;
import android.provider.ContactsContract.Directory;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnFocusChangeListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import com.android.common.widget.CompositeCursorAdapter.Partition;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.ContactListViewUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.contactphoto.ContactPhotoManager;
import com.android.dialer.performancereport.PerformanceReport;
import java.lang.ref.WeakReference;
import java.util.Locale;

/** Common base class for various contact-related list fragments. */
public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter> extends Fragment
    implements OnItemClickListener,
        OnScrollListener,
        OnFocusChangeListener,
        OnTouchListener,
        LoaderCallbacks<Cursor> {
  private static final String KEY_LIST_STATE = "liststate";
  private static final String KEY_SECTION_HEADER_DISPLAY_ENABLED = "sectionHeaderDisplayEnabled";
  private static final String KEY_PHOTO_LOADER_ENABLED = "photoLoaderEnabled";
  private static final String KEY_QUICK_CONTACT_ENABLED = "quickContactEnabled";
  private static final String KEY_ADJUST_SELECTION_BOUNDS_ENABLED = "adjustSelectionBoundsEnabled";
  private static final String KEY_INCLUDE_PROFILE = "includeProfile";
  private static final String KEY_SEARCH_MODE = "searchMode";
  private static final String KEY_VISIBLE_SCROLLBAR_ENABLED = "visibleScrollbarEnabled";
  private static final String KEY_SCROLLBAR_POSITION = "scrollbarPosition";
  private static final String KEY_QUERY_STRING = "queryString";
  private static final String KEY_DIRECTORY_SEARCH_MODE = "directorySearchMode";
  private static final String KEY_SELECTION_VISIBLE = "selectionVisible";
  private static final String KEY_DARK_THEME = "darkTheme";
  private static final String KEY_LEGACY_COMPATIBILITY = "legacyCompatibility";
  private static final String KEY_DIRECTORY_RESULT_LIMIT = "directoryResultLimit";

  private static final String DIRECTORY_ID_ARG_KEY = "directoryId";

  private static final int DIRECTORY_LOADER_ID = -1;

  private static final int DIRECTORY_SEARCH_DELAY_MILLIS = 300;
  private static final int DIRECTORY_SEARCH_MESSAGE = 1;

  private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20;
  private static final int STATUS_NOT_LOADED = 0;
  private static final int STATUS_LOADING = 1;
  private static final int STATUS_LOADED = 2;
  protected boolean mUserProfileExists;
  private boolean mSectionHeaderDisplayEnabled;
  private boolean mPhotoLoaderEnabled;
  private boolean mQuickContactEnabled = true;
  private boolean mAdjustSelectionBoundsEnabled = true;
  private boolean mIncludeProfile;
  private boolean mSearchMode;
  private boolean mVisibleScrollbarEnabled;
  private boolean mShowEmptyListForEmptyQuery;
  private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition();
  private String mQueryString;
  private int mDirectorySearchMode = DirectoryListLoader.SEARCH_MODE_NONE;
  private boolean mSelectionVisible;
  private boolean mLegacyCompatibility;
  private boolean mEnabled = true;
  private T mAdapter;
  private View mView;
  private ListView mListView;
  /** Used to save the scrolling state of the list when the fragment is not recreated. */
  private int mListViewTopIndex;

  private int mListViewTopOffset;
  /** Used for keeping track of the scroll state of the list. */
  private Parcelable mListState;

  private int mDisplayOrder;
  private int mSortOrder;
  private int mDirectoryResultLimit = DEFAULT_DIRECTORY_RESULT_LIMIT;
  private ContactPhotoManager mPhotoManager;
  private ContactsPreferences mContactsPrefs;
  private boolean mForceLoad;
  private boolean mDarkTheme;
  private int mDirectoryListStatus = STATUS_NOT_LOADED;

  /**
   * Indicates whether we are doing the initial complete load of data (false) or a refresh caused by
   * a change notification (true)
   */
  private boolean mLoadPriorityDirectoriesOnly;

  private Context mContext;

  private LoaderManager mLoaderManager;

  private Handler mDelayedDirectorySearchHandler;

  private static class DelayedDirectorySearchHandler extends Handler {
    private final WeakReference<ContactEntryListFragment<?>> contactEntryListFragmentRef;

    private DelayedDirectorySearchHandler(ContactEntryListFragment<?> contactEntryListFragment) {
      this.contactEntryListFragmentRef = new WeakReference<>(contactEntryListFragment);
    }

    @Override
    public void handleMessage(Message msg) {
      ContactEntryListFragment<?> contactEntryListFragment = contactEntryListFragmentRef.get();
      if (contactEntryListFragment == null) {
        return;
      }
      if (msg.what == DIRECTORY_SEARCH_MESSAGE) {
        contactEntryListFragment.loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj);
      }
    }
  }

  private ContactsPreferences.ChangeListener mPreferencesChangeListener =
      new ContactsPreferences.ChangeListener() {
        @Override
        public void onChange() {
          loadPreferences();
          reloadData();
        }
      };

  protected ContactEntryListFragment() {
    mDelayedDirectorySearchHandler = new DelayedDirectorySearchHandler(this);
  }

  protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);

  protected abstract T createListAdapter();

  /**
   * @param position Please note that the position is already adjusted for header views, so "0"
   *     means the first list item below header views.
   */
  protected abstract void onItemClick(int position, long id);

  @Override
  public void onAttach(Context context) {
    super.onAttach(context);
    setContext(context);
    setLoaderManager(super.getLoaderManager());
  }

  @Override
  public Context getContext() {
    return mContext;
  }

  /** Sets a context for the fragment in the unit test environment. */
  public void setContext(Context context) {
    mContext = context;
    configurePhotoLoader();
  }

  public void setEnabled(boolean enabled) {
    if (mEnabled != enabled) {
      mEnabled = enabled;
      if (mAdapter != null) {
        if (mEnabled) {
          reloadData();
        } else {
          mAdapter.clearPartitions();
        }
      }
    }
  }

  @Override
  public LoaderManager getLoaderManager() {
    return mLoaderManager;
  }

  /** Overrides a loader manager for use in unit tests. */
  public void setLoaderManager(LoaderManager loaderManager) {
    mLoaderManager = loaderManager;
  }

  public T getAdapter() {
    return mAdapter;
  }

  @Override
  public View getView() {
    return mView;
  }

  public ListView getListView() {
    return mListView;
  }

  @Override
  public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED, mSectionHeaderDisplayEnabled);
    outState.putBoolean(KEY_PHOTO_LOADER_ENABLED, mPhotoLoaderEnabled);
    outState.putBoolean(KEY_QUICK_CONTACT_ENABLED, mQuickContactEnabled);
    outState.putBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled);
    outState.putBoolean(KEY_INCLUDE_PROFILE, mIncludeProfile);
    outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
    outState.putBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED, mVisibleScrollbarEnabled);
    outState.putInt(KEY_SCROLLBAR_POSITION, mVerticalScrollbarPosition);
    outState.putInt(KEY_DIRECTORY_SEARCH_MODE, mDirectorySearchMode);
    outState.putBoolean(KEY_SELECTION_VISIBLE, mSelectionVisible);
    outState.putBoolean(KEY_LEGACY_COMPATIBILITY, mLegacyCompatibility);
    outState.putString(KEY_QUERY_STRING, mQueryString);
    outState.putInt(KEY_DIRECTORY_RESULT_LIMIT, mDirectoryResultLimit);
    outState.putBoolean(KEY_DARK_THEME, mDarkTheme);

    if (mListView != null) {
      outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState());
    }
  }

  @Override
  public void onCreate(Bundle savedState) {
    super.onCreate(savedState);
    restoreSavedState(savedState);
    mAdapter = createListAdapter();
    mContactsPrefs = new ContactsPreferences(mContext);
  }

  public void restoreSavedState(Bundle savedState) {
    if (savedState == null) {
      return;
    }

    mSectionHeaderDisplayEnabled = savedState.getBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED);
    mPhotoLoaderEnabled = savedState.getBoolean(KEY_PHOTO_LOADER_ENABLED);
    mQuickContactEnabled = savedState.getBoolean(KEY_QUICK_CONTACT_ENABLED);
    mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED);
    mIncludeProfile = savedState.getBoolean(KEY_INCLUDE_PROFILE);
    mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE);
    mVisibleScrollbarEnabled = savedState.getBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED);
    mVerticalScrollbarPosition = savedState.getInt(KEY_SCROLLBAR_POSITION);
    mDirectorySearchMode = savedState.getInt(KEY_DIRECTORY_SEARCH_MODE);
    mSelectionVisible = savedState.getBoolean(KEY_SELECTION_VISIBLE);
    mLegacyCompatibility = savedState.getBoolean(KEY_LEGACY_COMPATIBILITY);
    mQueryString = savedState.getString(KEY_QUERY_STRING);
    mDirectoryResultLimit = savedState.getInt(KEY_DIRECTORY_RESULT_LIMIT);
    mDarkTheme = savedState.getBoolean(KEY_DARK_THEME);

    // Retrieve list state. This will be applied in onLoadFinished
    mListState = savedState.getParcelable(KEY_LIST_STATE);
  }

  @Override
  public void onStart() {
    super.onStart();

    mContactsPrefs.registerChangeListener(mPreferencesChangeListener);

    mForceLoad = loadPreferences();

    mDirectoryListStatus = STATUS_NOT_LOADED;
    mLoadPriorityDirectoriesOnly = true;

    startLoading();
  }

  protected void startLoading() {
    if (mAdapter == null) {
      // The method was called before the fragment was started
      return;
    }

    configureAdapter();
    int partitionCount = mAdapter.getPartitionCount();
    for (int i = 0; i < partitionCount; i++) {
      Partition partition = mAdapter.getPartition(i);
      if (partition instanceof DirectoryPartition) {
        DirectoryPartition directoryPartition = (DirectoryPartition) partition;
        if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) {
          if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) {
            startLoadingDirectoryPartition(i);
          }
        }
      } else {
        getLoaderManager().initLoader(i, null, this);
      }
    }

    // Next time this method is called, we should start loading non-priority directories
    mLoadPriorityDirectoriesOnly = false;
  }

  @Override
  public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    if (id == DIRECTORY_LOADER_ID) {
      DirectoryListLoader loader = new DirectoryListLoader(mContext);
      loader.setDirectorySearchMode(mAdapter.getDirectorySearchMode());
      loader.setLocalInvisibleDirectoryEnabled(
          ContactEntryListAdapter.LOCAL_INVISIBLE_DIRECTORY_ENABLED);
      return loader;
    } else {
      CursorLoader loader = createCursorLoader(mContext);
      long directoryId =
          args != null && args.containsKey(DIRECTORY_ID_ARG_KEY)
              ? args.getLong(DIRECTORY_ID_ARG_KEY)
              : Directory.DEFAULT;
      mAdapter.configureLoader(loader, directoryId);
      return loader;
    }
  }

  public CursorLoader createCursorLoader(Context context) {
    return new CursorLoader(context, null, null, null, null, null) {
      @Override
      protected Cursor onLoadInBackground() {
        try {
          return super.onLoadInBackground();
        } catch (RuntimeException e) {
          // We don't even know what the projection should be, so no point trying to
          // return an empty MatrixCursor with the correct projection here.
          LogUtil.w(
              "ContactEntryListFragment.onLoadInBackground",
              "RuntimeException while trying to query ContactsProvider.");
          return null;
        }
      }
    };
  }

  private void startLoadingDirectoryPartition(int partitionIndex) {
    DirectoryPartition partition = (DirectoryPartition) mAdapter.getPartition(partitionIndex);
    partition.setStatus(DirectoryPartition.STATUS_LOADING);
    long directoryId = partition.getDirectoryId();
    if (mForceLoad) {
      if (directoryId == Directory.DEFAULT) {
        loadDirectoryPartition(partitionIndex, partition);
      } else {
        loadDirectoryPartitionDelayed(partitionIndex, partition);
      }
    } else {
      Bundle args = new Bundle();
      args.putLong(DIRECTORY_ID_ARG_KEY, directoryId);
      getLoaderManager().initLoader(partitionIndex, args, this);
    }
  }

  /**
   * Queues up a delayed request to search the specified directory. Since directory search will
   * likely introduce a lot of network traffic, we want to wait for a pause in the user's typing
   * before sending a directory request.
   */
  private void loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition) {
    mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE, partition);
    Message msg =
        mDelayedDirectorySearchHandler.obtainMessage(
            DIRECTORY_SEARCH_MESSAGE, partitionIndex, 0, partition);
    mDelayedDirectorySearchHandler.sendMessageDelayed(msg, DIRECTORY_SEARCH_DELAY_MILLIS);
  }

  /** Loads the directory partition. */
  protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) {
    Bundle args = new Bundle();
    args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId());
    getLoaderManager().restartLoader(partitionIndex, args, this);
  }

  /** Cancels all queued directory loading requests. */
  private void removePendingDirectorySearchRequests() {
    mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE);
  }

  @Override
  public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    if (!mEnabled) {
      return;
    }

    int loaderId = loader.getId();
    if (loaderId == DIRECTORY_LOADER_ID) {
      mDirectoryListStatus = STATUS_LOADED;
      mAdapter.changeDirectories(data);
      startLoading();
    } else {
      onPartitionLoaded(loaderId, data);
      if (isSearchMode()) {
        int directorySearchMode = getDirectorySearchMode();
        if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) {
          if (mDirectoryListStatus == STATUS_NOT_LOADED) {
            mDirectoryListStatus = STATUS_LOADING;
            getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this);
          } else {
            startLoading();
          }
        }
      } else {
        mDirectoryListStatus = STATUS_NOT_LOADED;
        getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
      }
    }
  }

  @Override
  public void onLoaderReset(Loader<Cursor> loader) {}

  protected void onPartitionLoaded(int partitionIndex, Cursor data) {
    if (partitionIndex >= mAdapter.getPartitionCount()) {
      // When we get unsolicited data, ignore it.  This could happen
      // when we are switching from search mode to the default mode.
      return;
    }

    // Return for non-"Suggestions" if on the zero-suggest screen.
    if (TextUtils.isEmpty(mQueryString) && partitionIndex > 0) {
      return;
    }

    mAdapter.changeCursor(partitionIndex, data);
    setProfileHeader();

    if (!isLoading()) {
      completeRestoreInstanceState();
    }
  }

  public boolean isLoading() {
    //noinspection SimplifiableIfStatement
    if (mAdapter != null && mAdapter.isLoading()) {
      return true;
    }

    return isLoadingDirectoryList();

  }

  public boolean isLoadingDirectoryList() {
    return isSearchMode()
        && getDirectorySearchMode() != DirectoryListLoader.SEARCH_MODE_NONE
        && (mDirectoryListStatus == STATUS_NOT_LOADED || mDirectoryListStatus == STATUS_LOADING);
  }

  @Override
  public void onStop() {
    super.onStop();
    mContactsPrefs.unregisterChangeListener();
    mAdapter.clearPartitions();
  }

  protected void reloadData() {
    removePendingDirectorySearchRequests();
    mAdapter.onDataReload();
    mLoadPriorityDirectoriesOnly = true;
    mForceLoad = true;
    startLoading();
  }

  /**
   * Shows a view at the top of the list with a pseudo local profile prompting the user to add a
   * local profile. Default implementation does nothing.
   */
  protected void setProfileHeader() {
    mUserProfileExists = false;
  }

  /** Provides logic that dismisses this fragment. The default implementation does nothing. */
  protected void finish() {}

  public boolean isSectionHeaderDisplayEnabled() {
    return mSectionHeaderDisplayEnabled;
  }

  public void setSectionHeaderDisplayEnabled(boolean flag) {
    if (mSectionHeaderDisplayEnabled != flag) {
      mSectionHeaderDisplayEnabled = flag;
      if (mAdapter != null) {
        mAdapter.setSectionHeaderDisplayEnabled(flag);
      }
      configureVerticalScrollbar();
    }
  }

  public boolean isVisibleScrollbarEnabled() {
    return mVisibleScrollbarEnabled;
  }

  public void setVisibleScrollbarEnabled(boolean flag) {
    if (mVisibleScrollbarEnabled != flag) {
      mVisibleScrollbarEnabled = flag;
      configureVerticalScrollbar();
    }
  }

  private void configureVerticalScrollbar() {
    boolean hasScrollbar = isVisibleScrollbarEnabled() && isSectionHeaderDisplayEnabled();

    if (mListView != null) {
      mListView.setFastScrollEnabled(hasScrollbar);
      mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition);
      mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
    }
  }

  public boolean isPhotoLoaderEnabled() {
    return mPhotoLoaderEnabled;
  }

  public void setPhotoLoaderEnabled(boolean flag) {
    mPhotoLoaderEnabled = flag;
    configurePhotoLoader();
  }

  public void setQuickContactEnabled(boolean flag) {
    this.mQuickContactEnabled = flag;
  }

  public void setAdjustSelectionBoundsEnabled(boolean flag) {
    mAdjustSelectionBoundsEnabled = flag;
  }

  public final boolean isSearchMode() {
    return mSearchMode;
  }

  /**
   * Enter/exit search mode. This is method is tightly related to the current query, and should only
   * be called by {@link #setQueryString}.
   *
   * <p>Also note this method doesn't call {@link #reloadData()}; {@link #setQueryString} does it.
   */
  protected void setSearchMode(boolean flag) {
    if (mSearchMode != flag) {
      mSearchMode = flag;

      if (!flag) {
        mDirectoryListStatus = STATUS_NOT_LOADED;
        getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
      }

      if (mAdapter != null) {
        mAdapter.setSearchMode(flag);

        mAdapter.clearPartitions();
        if (!flag) {
          // If we are switching from search to regular display, remove all directory
          // partitions after default one, assuming they are remote directories which
          // should be cleaned up on exiting the search mode.
          mAdapter.removeDirectoriesAfterDefault();
        }
        mAdapter.configurePartitionsVisibility(flag);
      }

      if (mListView != null) {
        mListView.setFastScrollEnabled(!flag);
      }
    }
  }

  @Nullable
  public final String getQueryString() {
    return mQueryString;
  }

  public void setQueryString(String queryString) {
    if (!TextUtils.equals(mQueryString, queryString)) {
      if (mShowEmptyListForEmptyQuery && mAdapter != null && mListView != null) {
        if (TextUtils.isEmpty(mQueryString)) {
          // Restore the adapter if the query used to be empty.
          mListView.setAdapter(mAdapter);
        } else if (TextUtils.isEmpty(queryString)) {
          // Instantly clear the list view if the new query is empty.
          mListView.setAdapter(null);
        }
      }

      mQueryString = queryString;
      setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery);

      if (mAdapter != null) {
        mAdapter.setQueryString(queryString);
        reloadData();
      }
    }
  }

  public void setShowEmptyListForNullQuery(boolean show) {
    mShowEmptyListForEmptyQuery = show;
  }

  public boolean getShowEmptyListForNullQuery() {
    return mShowEmptyListForEmptyQuery;
  }

  public int getDirectoryLoaderId() {
    return DIRECTORY_LOADER_ID;
  }

  public int getDirectorySearchMode() {
    return mDirectorySearchMode;
  }

  public void setDirectorySearchMode(int mode) {
    mDirectorySearchMode = mode;
  }

  protected int getContactNameDisplayOrder() {
    return mDisplayOrder;
  }

  protected void setContactNameDisplayOrder(int displayOrder) {
    mDisplayOrder = displayOrder;
    if (mAdapter != null) {
      mAdapter.setContactNameDisplayOrder(displayOrder);
    }
  }

  public int getSortOrder() {
    return mSortOrder;
  }

  public void setSortOrder(int sortOrder) {
    mSortOrder = sortOrder;
    if (mAdapter != null) {
      mAdapter.setSortOrder(sortOrder);
    }
  }

  public void setDirectoryResultLimit(int limit) {
    mDirectoryResultLimit = limit;
  }

  protected boolean loadPreferences() {
    boolean changed = false;
    if (getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) {
      setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
      changed = true;
    }

    if (getSortOrder() != mContactsPrefs.getSortOrder()) {
      setSortOrder(mContactsPrefs.getSortOrder());
      changed = true;
    }

    return changed;
  }

  @Override
  public View onCreateView(
      LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    onCreateView(inflater, container);

    boolean searchMode = isSearchMode();
    mAdapter.setSearchMode(searchMode);
    mAdapter.configurePartitionsVisibility(searchMode);
    mAdapter.setPhotoLoader(mPhotoManager);
    mListView.setAdapter(mAdapter);
    return mView;
  }

  protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
    mView = inflateView(inflater, container);

    mListView = mView.findViewById(android.R.id.list);
    if (mListView == null) {
      throw new RuntimeException(
          "Your content must have a ListView whose id attribute is " + "'android.R.id.list'");
    }

    View emptyView = mView.findViewById(android.R.id.empty);
    if (emptyView != null) {
      mListView.setEmptyView(emptyView);
    }

    mListView.setOnItemClickListener(this);
    mListView.setOnFocusChangeListener(this);
    mListView.setOnTouchListener(this);
    mListView.setFastScrollEnabled(!isSearchMode());

    // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
    // them when an A-Z headers is visible.
    mListView.setDividerHeight(0);

    // We manually save/restore the listview state
    mListView.setSaveEnabled(false);

    configureVerticalScrollbar();
    configurePhotoLoader();

    getAdapter().setFragmentRootView(getView());

    ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, mView);
  }

  @Override
  public void onHiddenChanged(boolean hidden) {
    super.onHiddenChanged(hidden);
    if (getActivity() != null && getView() != null && !hidden) {
      // If the padding was last applied when in a hidden state, it may have been applied
      // incorrectly. Therefore we need to reapply it.
      ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, getView());
    }
  }

  protected void configurePhotoLoader() {
    if (isPhotoLoaderEnabled() && mContext != null) {
      if (mPhotoManager == null) {
        mPhotoManager = ContactPhotoManager.getInstance(mContext);
      }
      if (mListView != null) {
        mListView.setOnScrollListener(this);
      }
      if (mAdapter != null) {
        mAdapter.setPhotoLoader(mPhotoManager);
      }
    }
  }

  protected void configureAdapter() {
    if (mAdapter == null) {
      return;
    }

    mAdapter.setQuickContactEnabled(mQuickContactEnabled);
    mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled);
    mAdapter.setQueryString(mQueryString);
    mAdapter.setDirectorySearchMode(mDirectorySearchMode);
    mAdapter.setPinnedPartitionHeadersEnabled(false);
    mAdapter.setContactNameDisplayOrder(mDisplayOrder);
    mAdapter.setSortOrder(mSortOrder);
    mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled);
    mAdapter.setSelectionVisible(mSelectionVisible);
    mAdapter.setDirectoryResultLimit(mDirectoryResultLimit);
    mAdapter.setDarkTheme(mDarkTheme);
  }

  @Override
  public void onScroll(
      AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}

  @Override
  public void onScrollStateChanged(AbsListView view, int scrollState) {
    PerformanceReport.recordScrollStateChange(scrollState);
    if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
      mPhotoManager.pause();
    } else if (isPhotoLoaderEnabled()) {
      mPhotoManager.resume();
    }
  }

  @Override
  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    hideSoftKeyboard();

    int adjPosition = position - mListView.getHeaderViewsCount();
    if (adjPosition >= 0) {
      onItemClick(adjPosition, id);
    }
  }

  private void hideSoftKeyboard() {
    // Hide soft keyboard, if visible
    InputMethodManager inputMethodManager =
        (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
    inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0);
  }

  /** Dismisses the soft keyboard when the list takes focus. */
  @Override
  public void onFocusChange(View view, boolean hasFocus) {
    if (view == mListView && hasFocus) {
      hideSoftKeyboard();
    }
  }

  /** Dismisses the soft keyboard when the list is touched. */
  @Override
  public boolean onTouch(View view, MotionEvent event) {
    if (view == mListView) {
      hideSoftKeyboard();
    }
    return false;
  }

  @Override
  public void onPause() {
    // Save the scrolling state of the list view
    mListViewTopIndex = mListView.getFirstVisiblePosition();
    View v = mListView.getChildAt(0);
    mListViewTopOffset = (v == null) ? 0 : (v.getTop() - mListView.getPaddingTop());

    super.onPause();
    removePendingDirectorySearchRequests();
  }

  @Override
  public void onResume() {
    super.onResume();
    // Restore the selection of the list view. See a bug.
    // This has to be done manually because if the list view has its emptyView set,
    // the scrolling state will be reset when clearPartitions() is called on the adapter.
    mListView.setSelectionFromTop(mListViewTopIndex, mListViewTopOffset);
  }

  /** Restore the list state after the adapter is populated. */
  protected void completeRestoreInstanceState() {
    if (mListState != null) {
      mListView.onRestoreInstanceState(mListState);
      mListState = null;
    }
  }

  public void setDarkTheme(boolean value) {
    mDarkTheme = value;
    if (mAdapter != null) {
      mAdapter.setDarkTheme(value);
    }
  }

  private int getDefaultVerticalScrollbarPosition() {
    final Locale locale = Locale.getDefault();
    final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
    switch (layoutDirection) {
      case View.LAYOUT_DIRECTION_RTL:
        return View.SCROLLBAR_POSITION_LEFT;
      case View.LAYOUT_DIRECTION_LTR:
      default:
        return View.SCROLLBAR_POSITION_RIGHT;
    }
  }
}
